package com.wangl.spring.service;

import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.lang.properties.psi.PropertiesFile;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.roots.OrderEnumerator;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileVisitor;
import com.intellij.psi.*;
import com.wangl.spring.service.chain.SuggestionTreeBuilderChain;
import com.wangl.spring.service.resolver.impl.TransactionalEventListenerResolver;
import com.wangl.spring.suggestion.PropertiesManager;
import com.wangl.spring.suggestion.SuggestionKeyNode;
import com.wangl.spring.suggestion.SuggestionNodeTree;
import com.wangl.spring.utils.PsiCustomUtil;
import org.apache.commons.lang.time.StopWatch;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

import static com.intellij.openapi.application.ApplicationManager.getApplication;
import static com.wangl.spring.utils.PsiCustomUtil.*;

/**
 * @ClassName SuggestionService
 * @Description TODO
 * @Author wangl
 * @Date 2023/3/31 10:49
 */
public class SuggestionService {

    private final Module module;
    private final SuggestionNodeTree suggestionNodeTree;
    private final Set<String> indexed;
    private final SuggestionTreeBuilderChain suggestionTreeBuilderChain;
    private final ThreadPoolExecutor pool;
    private final PropertiesManager propertiesManager;
    private volatile boolean canSuggest;

    public SuggestionService(Module module) {
        this.module = module;
        this.propertiesManager = module.getProject().getService(ProjectSuggestionService.class).getPropertiesManager();
        this.suggestionNodeTree = new SuggestionNodeTree();
        this.indexed = new CopyOnWriteArraySet<>();
        this.suggestionTreeBuilderChain = SuggestionTreeBuilderChain.getInstance();
        pool = new ThreadPoolExecutor(8, 8, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy());
    }

    public void reIndex(){
        indexed.clear();
        suggestionNodeTree.clear();
        canSuggest = false;
        DumbService.getInstance(module.getProject()).runWhenSmart(() -> {
            StopWatch moduleTimer = new StopWatch();
            moduleTimer.start();
            List<CompletableFuture<String>> futures = new ArrayList<>();
            try {
                OrderEnumerator moduleOrderEnumerator = OrderEnumerator.orderEntries(module);
                futures.addAll(rebuildSuggestionNodeTree(moduleOrderEnumerator));
                futures.addAll(resolveJavaSources(moduleOrderEnumerator));
                futures.addAll(resolveResources(moduleOrderEnumerator));
            } finally {
                CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
                        .whenComplete((res, ex) -> {
                            canSuggest = true;
                            moduleTimer.stop();
                            System.out.println(Thread.currentThread().getName() + " module：" + module.getName() + ", index " + moduleTimer);
                        });
            }
        });
    }

    private List<CompletableFuture<String>> resolveResources(OrderEnumerator moduleOrderEnumerator) {
        List<CompletableFuture<String>> futures = new ArrayList<>();
        for (VirtualFile virtualFile : moduleOrderEnumerator.sources().getRoots()) {
            if (!virtualFile.getPath().contains("/src/main/resources")){
                continue;
            }
            VfsUtilCore.visitChildrenRecursively(virtualFile, new VirtualFileVisitor<Void>() {
                @Override
                public boolean visitFile(@NotNull VirtualFile file) {
                    futures.add(CompletableFuture.supplyAsync(() -> {
                        DumbService.getInstance(module.getProject()).runReadActionInSmartMode(() -> {
                            if (!file.isDirectory()){
                                PsiFile psiFile = PsiManager.getInstance(module.getProject()).findFile(file);
                                if (psiFile instanceof PropertiesFile && file.getName().matches("application(-\\w*)?\\.properties")){
                                    propertiesManager.insert((PropertiesFile) psiFile, module);
                                }
                            }
                        });
                        return file.getPath();
                    }, pool));
                    return true;
                }
            });
        }
        return futures;
    }

    private List<CompletableFuture<String>> resolveJavaSources(OrderEnumerator moduleOrderEnumerator) {
        List<CompletableFuture<String>> futures = new ArrayList<>();
        for (VirtualFile virtualFile : moduleOrderEnumerator.sources().getRoots()) {
            if (!virtualFile.getPath().contains("/src/main/java")){
                continue;
            }
            VfsUtilCore.visitChildrenRecursively(virtualFile, new VirtualFileVisitor() {
                @Override
                public boolean visitFile(@NotNull VirtualFile file) {
                    futures.add(CompletableFuture.supplyAsync(() -> {
                        DumbService.getInstance(module.getProject()).runReadActionInSmartMode(() -> {
                            if (!file.isDirectory()){
                                PsiFile psiFile = PsiManager.getInstance(module.getProject()).findFile(file);
                                if (psiFile instanceof PsiJavaFile && Objects.equals(psiFile.getVirtualFile().getExtension(), JavaFileType.DEFAULT_EXTENSION)){
                                    PsiJavaFile psiJavaFile = (PsiJavaFile) psiFile;
                                    PsiClass[] classes = psiJavaFile.getClasses();
                                    for (PsiClass psiClass : classes) {
                                        TransactionalEventListenerResolver.resolve(psiClass);
                                        suggestionTreeBuilderChain.build(suggestionNodeTree, psiClass);
                                    }
                                }
                            }
                        });
                        return file.getPath();
                    }, pool));
                    return true;
                }
            });
        }
        return futures;
    }

    private List<CompletableFuture<String>> rebuildSuggestionNodeTree(OrderEnumerator moduleOrderEnumerator) {
        List<CompletableFuture<String>> futures = new ArrayList<>();
        for (VirtualFile virtualFile : moduleOrderEnumerator.recursively().classes().getRoots()) {
            if (indexed.contains(virtualFile.getPath()) ||
                    virtualFile.getPath().contains("/classes/java/main") ||
                    virtualFile.getPath().contains("/classes/java/test") ||
                    virtualFile.getPath().contains("/jdk")){
                continue;
            }
            indexed.add(virtualFile.getPath());
            VfsUtilCore.visitChildrenRecursively(virtualFile, new VirtualFileVisitor() {
                @Override
                public boolean visitFile(@NotNull VirtualFile file) {
                    futures.add(CompletableFuture.supplyAsync(() -> {
                        DumbService.getInstance(module.getProject()).runReadActionInSmartMode(() -> {
                            if (!file.isDirectory()){
                                PsiFile psiFile = PsiManager.getInstance(module.getProject()).findFile(file);
                                if (psiFile instanceof PsiJavaFile){
                                    PsiJavaFile psiJavaFile = (PsiJavaFile) psiFile;
                                    PsiClass[] classes = psiJavaFile.getClasses();
                                    for (PsiClass psiClass : classes) {
                                        suggestionTreeBuilderChain.build(suggestionNodeTree, psiClass);
                                    }
                                }
                            }
                        });
                        return file.getPath();
                    }, pool));
                    return true;
                }
            });
        }
        return futures;
    }

    public List<String> getSuggestionsByPrefix(String prefix){
        suggestionNodeTree.rebuildTree();
        Set<String> result = new HashSet<>();
        List<SuggestionKeyNode> suggestionKeyNodes = suggestByPrefix(prefix);
        if (!CollectionUtils.isEmpty(suggestionKeyNodes)){
            result.addAll(suggestionKeyNodes.stream()
                    .map(SuggestionKeyNode::getSuggestion)
                    .collect(Collectors.toList()));
        }

        int lastIndexOf = prefix.lastIndexOf(".");
        if (lastIndexOf == -1){
            return new ArrayList<>(result);
        }
        prefix = prefix.substring(0, lastIndexOf);
        String[] groups = prefix.split("\\.");
        List<String> dynamicGroup = new ArrayList<>();
        for (String group : groups) {
            dynamicGroup.add(group);
            String dynamicPrefix = String.join(".", dynamicGroup);
            Set<PsiElement> configurationPropertiesElements = getConfigurationPropertiesElement(dynamicPrefix);
            if (CollectionUtils.isEmpty(configurationPropertiesElements)){
                continue;
            }
            for (PsiElement configurationPropertiesElement : configurationPropertiesElements) {
                if (configurationPropertiesElement instanceof PsiClass){
                    PsiClass configClass = (PsiClass) configurationPropertiesElement;
                    String finalPrefix = prefix;
                    result.addAll(findAllWritableMethodByConfigPrefix(prefix, configClass, dynamicPrefix).stream()
                            .flatMap(psiMethod -> PsiCustomUtil.getSuggestionByWritableMethod(psiMethod).stream())
                            .map(suggestion -> finalPrefix + "." + suggestion)
                            .collect(Collectors.toList()));
                }
                if (configurationPropertiesElement instanceof PsiMethod){
                    PsiMethod configMethod = (PsiMethod) configurationPropertiesElement;
                    PsiClass configurableClass = PsiCustomUtil.getConfigurableClassByPsiType(configMethod.getReturnType());
                    if (configurableClass == null){
                        continue;
                    }
                    result.addAll(findAllWritableMethodByConfigPrefix(prefix, configurableClass, dynamicPrefix).stream()
                            .flatMap(psiMethod -> PsiCustomUtil.getSuggestionByWritableMethod(psiMethod).stream())
                            .map(suggestion -> dynamicPrefix + "." + suggestion)
                            .collect(Collectors.toList()));
                }
            }
        }
        return new ArrayList<>(result);
    }

    public static Set<PsiMethod> findAllWritableMethodByConfigPrefix(String prefix, PsiClass psiClass, String configPrefix){
        if (Objects.equals(prefix, configPrefix)){
            return findAllWritableMethod(psiClass);
        }
        prefix = prefix.substring(configPrefix.length() + 1);
        String[] groups = prefix.split("\\.");
        int i = 0;
        PsiMethod writableMethod = null;
        while (i < groups.length){
            writableMethod = findWritableMethod(psiClass, groups[i]);
            if (writableMethod == null){
                return Collections.emptySet();
            }
            PsiClass propertyPsiClass = PsiCustomUtil.getPropertyClassByMethod(writableMethod);
            if (propertyPsiClass == null){
                return Collections.emptySet();
            }
            psiClass = propertyPsiClass;
            i++;
        }
        if (writableMethod == null){
            return Collections.emptySet();
        }
        PsiClass propertyPsiClass = PsiCustomUtil.getPropertyClassByMethod(writableMethod);
        if (propertyPsiClass == null){
            return Collections.emptySet();
        }
        return findAllWritableMethod(psiClass);
    }

    public PsiMethod findWritableMethodByPrefix(String prefix, PsiClass psiClass, String configPrefix){
        prefix = prefix.substring(configPrefix.length() + 1);
        String[] groups = prefix.split("\\.");
        int i = 0;
        PsiMethod writableMethod = null;
        while (i < groups.length){
            writableMethod = findWritableMethod(psiClass, groups[i]);
            if (writableMethod == null){
                return null;
            }
            PsiType propertyType = PsiCustomUtil.getWritablePropertyType(writableMethod.getContainingClass(), writableMethod);
            if (propertyType instanceof PsiPrimitiveType && i == groups.length - 1){
                return writableMethod;
            }
            PsiClass propertyPsiClass = PsiCustomUtil.getPropertyClassByMethod(writableMethod);
            if (propertyPsiClass == null){
                return null;
            }
            psiClass = propertyPsiClass;
            i++;
        }
        return writableMethod;
    }


    public List<SuggestionKeyNode> suggestByPrefix(String prefix){
        if (!prefix.contains(".")){
            return suggestionNodeTree.getRootNodes().stream()
                    .filter(node -> node.getText().contains(prefix))
                    .collect(Collectors.toList());
        }
        int index = prefix.lastIndexOf(".");
        SuggestionKeyNode suggestionKeyNode = suggestionNodeTree.searchNode(prefix.substring(0, index));
        if (suggestionKeyNode == null){
            return Collections.emptyList();
        }
        if (index == prefix.length() - 1){
            return suggestionKeyNode.getChildren();
        }
        return suggestionKeyNode.getChildren().stream()
                .filter(node -> node.getText().startsWith(prefix.substring(index + 1)))
                .collect(Collectors.toList());
    }

    public Set<PsiElement> getConfigurationPropertiesElement(String prefix){
        return suggestionNodeTree.getConfigurationPropertiesElement(prefix);
    }

    public Set<PsiElement> searchByPropertyName(String propertyName){
        suggestionNodeTree.rebuildTree();
        Set<PsiElement> targets = suggestionNodeTree.searchTargets(propertyName);
        if (!CollectionUtils.isEmpty(targets)){
            return targets;
        }
        String[] groups = propertyName.split("\\.");
        List<String> dynamicGroup = new ArrayList<>();
        Set<PsiElement> targetElements = new HashSet<>();
        for (String group : groups) {
            dynamicGroup.add(group);
            String dynamicPrefix = String.join(".", dynamicGroup);
            Set<PsiElement> configurationPropertiesElements = getConfigurationPropertiesElement(dynamicPrefix);
            if (CollectionUtils.isEmpty(configurationPropertiesElements)){
                continue;
            }
            for (PsiElement configurationPropertiesElement : configurationPropertiesElements) {
                if (configurationPropertiesElement instanceof PsiClass){
                    PsiClass configClass = (PsiClass) configurationPropertiesElement;
                    PsiMethod writableMethod = findWritableMethodByPrefix(propertyName, configClass, dynamicPrefix);
                    if (writableMethod != null){
                        targetElements.add(writableMethod);
                    }
                }
                if (configurationPropertiesElement instanceof PsiMethod){
                    PsiMethod configMethod = (PsiMethod) configurationPropertiesElement;
                    PsiClass configurableClass = PsiCustomUtil.getConfigurableClassByPsiType(configMethod.getReturnType());
                    if (configurableClass == null){
                        continue;
                    }
                    PsiMethod writableMethod = findWritableMethodByPrefix(propertyName, configurableClass, dynamicPrefix);
                    if (writableMethod != null){
                        targetElements.add(writableMethod);
                    }
                }
            }
        }
        return targetElements;
    }

    public SuggestionNodeTree getSuggestionNodeTree() {
        return suggestionNodeTree;
    }

    public PropertiesManager getPropertiesManager() {
        return propertiesManager;
    }

    public boolean isCanSuggest(){
        return canSuggest;
    }
}
